package xj.meta;

/**********************************************************************************
 *
 * @author Xiaoping Jia
 * @version 0.2
 *
 * Created on: 2005/05/01
 * Requires: J2SE 5.0
 *
 **********************************************************************************/

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import xj.util.xml.XMLUtil;

public class ModelBuilder {

  public static ModelElement loadModelFile(String infile) {
    if (infile != null) {
      if (debug) {
	System.out.println("ModelBuilder.loadModelFile");
      }

      Document xmldoc = XMLUtil.createXMLDocument(infile, false);
      if (xmldoc != null) {
	if (debug) {
	  System.out.println("ModelBuilder.loadModelFile: xmldoc != null");
	}

	Element docElement = xmldoc.getDocumentElement();
	ModelElement root = loadElementFromXML(docElement);

	ModelRepository rep = ModelRepository.getInstance();
	Iterator<ModelElement> iter = rep.elementIterator();
	while (iter.hasNext()) {
	  ModelElement e = iter.next();
	  if (e instanceof ModelReference) {
	    ModelReference ref = (ModelReference) e;
	    ref.resolveLink();
	  }
	}
	return root;
      }
    }
    return null;
  }

  public static ModelElement loadElementFromXML(Node node) {
    ModelElement model = null;
    if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
      String name = node.getLocalName();
      String className = elementMap.get(name);

      if (debug) {
	System.out.println("ModelBuilder.loadElementFromXML nodename=" + name + 
			   " classname=" + className);
      }

      if (className == null) {
	className = name;
      }
      Class modelClass = null;
      try {
	modelClass = Class.forName(className);
	Object obj = modelClass.newInstance();
	if (obj instanceof ModelElement) {
	  model = (ModelElement) obj;
	}
      } catch (ClassNotFoundException e) {
	System.err.println("Error ModelBuilder.loadElementFromXML nodename=" + name + 
			   " classname=" + className);
	e.printStackTrace();
      } catch (InstantiationException e) {
	System.err.println("Error ModelBuilder.loadElementFromXML nodename=" + name + 
			   " classname=" + className);
	e.printStackTrace();
      } catch (IllegalAccessException e) {
	System.err.println("Error ModelBuilder.loadElementFromXML nodename=" + name + 
			   " classname=" + className);
	e.printStackTrace();
      }

      if (model != null) {
	NamedNodeMap attrs = node.getAttributes();
	if (attrs != null) {
	  int n = attrs.getLength();
	  for (int i = 0; i < n; i++) {
	    Node attrNode = attrs.item(i);
	    String attrName = attrNode.getNodeName();
	    String attrValue = attrNode.getNodeValue();

	    if (attrName != null && attrValue != null) {
	      Class type = getModelPropertyType(model, attrName);

	      if (type != null) {
		Object value = stringToObject(attrValue, type);

		if (debug) {
		  System.out.println("ModelBuilder.loadElementFromXML attr  name=" + attrName + 
				     "; value=" + attrValue + 
				     "; type=" + (type != null ? type.getName() : "null") + 
				     "; value type=" + value.getClass().getName());
		}

		setModelPropertyValue(model, attrName, value);
	      } else {
		// collection of strings
		Method method = getAdder(modelClass, attrName, String.class);
		if (method != null) {
		  try {
		    if (debug) {
		      System.out.println("ModelBuilder.loadElementFromXML invoke method=" + 
					 method.getName());
		    }

		    method.invoke(model, attrValue);
		  } catch (InvocationTargetException e) {
		    e.printStackTrace();
		  } catch (IllegalAccessException e) {
		    e.printStackTrace();
		  }
		}
	      }
	    }

	  }
	}

	if (node.hasChildNodes()) {
	  NodeList nodes = node.getChildNodes();
	  if (nodes != null) {
	    int n = nodes.getLength();
	    for (int i = 0; i < n; i++) {
	      Node cnode = nodes.item(i);
	      if (cnode.getNodeType() == Node.ELEMENT_NODE) {
		String cnodeName = cnode.getLocalName();

		if (debug)
		  System.out.println("ModelBuilder.loadElementFromXML element  name=" + cnodeName);

		Class type;
		if (elementMap.containsKey(cnodeName)) {
		  ModelElement childModel = loadElementFromXML(cnode);
		  type = childModel.getClass();
		  Method method = getAdder(modelClass, cnodeName, type);
		  if (method != null) {
		    try {
		      if (debug)
			System.out.println("ModelBuilder.loadElementFromXML invoke method=" + method.getName());


		      method.invoke(model, childModel);
		    } catch (InvocationTargetException e) {
		      e.printStackTrace();
		    } catch (IllegalAccessException e) {
		      e.printStackTrace();
		    }
		  }
		} else if (AbstractModelElement.STEREOTYPES_ELEMENT_NAME.equals(cnodeName) || 
			   AbstractModelElement.TAGS_ELEMENT_NAME.equals(cnodeName)) {
		  NodeList snodes = cnode.getChildNodes();
		  if (snodes != null) {
		    int sn = snodes.getLength();
		    for (int j = 0; j < sn; j++) {
		      org.w3c.dom.Node snode = snodes.item(j);
		      if (snode.getNodeType() == Node.ELEMENT_NODE) {
			Element e1 = (Element) snode;
			String ename = e1.getLocalName();
			if (AbstractModelElement.STEREOTYPE_ELEMENT_NAME.equals(ename)) {
			  String st = e1.getAttribute("name");
			  model.addStereotype(st);
			} else if (AbstractModelElement.TAG_ELEMENT_NAME.equals(ename)) {
			  String tname = e1.getAttribute("name");
			  String tval = e1.getAttribute("value");
			  model.setTagValue(tname, tval);
			}
		      }
		    }
		  }
		} else if (AbstractModelElement.LINK_ELEMENT_NAME.equals(cnodeName)) {
		  if (cnode.getNodeType() == Node.ELEMENT_NODE) {
		    Element e1 = (Element) cnode;
		    String lname = e1.getAttribute("name");
		    String llink = e1.getAttribute("href");
		    model.addLink(lname, llink);
		  }
		} else {
		  type = getModelPropertyType(model, cnodeName);
		  if (type == null) {
		    type = String.class;
		  }
		  String text = cnode.getTextContent();
		  if (text != null) {
		    text = text.trim();
		  }
		  Object value = stringToObject(text, type);
		  // setModelPropertyValue(model, cnodeName,
		  // value);
		  Method method = getAdder(modelClass, cnodeName, type);
		  if (method != null) {
		    try {
		      method.invoke(model, value);
		    } catch (InvocationTargetException e) {
		      e.printStackTrace();
		    } catch (IllegalAccessException e) {
		      e.printStackTrace();
		    }
		  }
		}
	      }
	    }
	  }
	}
      }
    }
    return model;
  }

  protected static String getAdderName(String elementName, boolean collection) {
    if (elementName != null && elementName.length() > 0) {
      char c = elementName.charAt(0);
      c = Character.toUpperCase(c);
      String name = ("" + c + elementName.substring(1));
      if (collection) {
	return "add" + name;
      } else {
	return "set" + name;
      }
    }
    return null;
  }

  protected static Method getAdder(Class modelClass, String elementName,
      Class elementClass) {
    Method method = null;
    if (modelClass != null && elementClass != null && elementName != null
	&& elementName.length() > 0) {
      String name = elementName;
      String adderName = adderMap.get(name);
      while (adderName != null) {
	name = adderName;
	adderName = adderMap.get(name);
      }

      char c = name.charAt(0);
      c = Character.toUpperCase(c);
      name = ("" + c + name.substring(1));
      String mname = "set" + name;
      method = getMethod(modelClass, mname, elementClass);
      if (method == null) {
	mname = "add" + name;
	method = getMethod(modelClass, mname, elementClass);
      }
    }
    return method;
  }

  protected static Method getMethod(Class c, String name, Class p) {
    if (c != null && name != null && p != null) {
      Method[] methods = c.getMethods();
      if (methods != null) {
	int n = methods.length;
	for (int i = 0; i < n; i++) {
	  Method m = methods[i];
	  String mn = m.getName();
	  if (name.equals(mn)) {
	    Class[] params = m.getParameterTypes();
	    if (params != null && params.length == 1) {
	      if (params[0].isAssignableFrom(p)) {
		return m;
	      }
	    }
	  }
	}
      }
    }
    return null;
  }

  public static void addElement(String elementName, String elementClassName) {
    if (elementName != null && elementClassName != null) {
      if (false) {
	System.out.println("ModelBuilder.addElement elementName=" + elementName
	    + " className=" + elementClassName);
      }

      elementMap.put(elementName, elementClassName);
    }
  }

  public static void addElements(String[] elementClassNames) {
    if (elementClassNames != null) {
      for (int i = 0; i < elementClassNames.length; i++) {
	String className = elementClassNames[i];
	String elementName = className;
	int k = elementName.lastIndexOf('.');
	if (k >= 0) {
	  elementName = elementName.substring(k + 1);
	}
	addElement(elementName, className);
      }
    }
  }

  public static void addAdderName(String elementName, String adderName) {
    if (elementName != null && adderName != null) {
      if (false) {
	System.out.println("ModelBuilder.addAdderName elementName="
	    + elementName + " adderName=" + adderName);
      }

      adderMap.put(elementName, adderName);
    }
  }

  public static void addAdderNames(String[][] adderNames) {
    if (adderNames != null) {
      for (int i = 0; i < adderNames.length; i++) {
	String[] pair = adderNames[i];
	if (pair != null && pair.length >= 2) {
	  String elementName = pair[0];
	  String adderName = pair[1];
	  addAdderName(elementName, adderName);
	}
      }
    }
  }

  public static Object stringToObject(String value, Class type) {
    if (value != null && type != null) {
      if (type == Boolean.class || type == boolean.class) {
	return Boolean.valueOf(value);
      } else if (type == Byte.class || type == byte.class) {
	return Byte.valueOf(value);
      } else if (type == Character.class || type == char.class) {
	if (value.length() > 0) {
	  return Character.valueOf(value.charAt(0));
	} else {
	  return '\000';
	}
      } else if (type == Double.class || type == double.class) {
	return Double.valueOf(value);
      } else if (type == Float.class || type == float.class) {
	return Float.valueOf(value);
      } else if (type == Integer.class || type == int.class) {
	return Integer.valueOf(value);
      } else if (type == Long.class || type == long.class) {
	return Long.valueOf(value);
      } else if (type == Short.class || type == short.class) {
	return Short.valueOf(value);
      } else if (type == xj.model.uml.Multiplicity.class) {
	return xj.model.uml.Multiplicity.toMultiplicity(value);
      } else if (Enum.class.isAssignableFrom(type)) {
	try { 
	  return Enum.valueOf(type, value);
	} catch (IllegalArgumentException e) {
	  return null;
	}
      }
    }
    return value;
  }

  public static Class getModelPropertyType(ModelElement model, String fieldName) {
    if (model != null && fieldName != null) {
      try {
	Class cls = model.getClass();
	PropertyDescriptor prop = new PropertyDescriptor(fieldName, cls);
	return prop.getPropertyType();
      } catch (IntrospectionException e) {
	if (debug) {
	  e.printStackTrace();
	}
      }
    }
    return null;
  }

  public static Object getModelPropertyValue(ModelElement model,
					     String fieldName) {
    if (model != null && fieldName != null) {
      try {
	Class cls = model.getClass();
	PropertyDescriptor prop = new PropertyDescriptor(fieldName, cls);
	Method read = prop.getReadMethod();
	return read.invoke(model);
      } catch (IntrospectionException e) {
	e.printStackTrace();
      } catch (InvocationTargetException e) {
	e.printStackTrace();
      } catch (IllegalAccessException e) {
	e.printStackTrace();
      }
    }
    return null;
  }

  public static void setModelPropertyValue(ModelElement model,
					   String fieldName, Object value) {
    if (model != null && fieldName != null) {
      try {
	Class cls = model.getClass();
	PropertyDescriptor prop = new PropertyDescriptor(fieldName, cls);
	Method write = prop.getWriteMethod();
	write.invoke(model, value);
      } catch (IntrospectionException e) {
	e.printStackTrace();
      } catch (InvocationTargetException e) {
	e.printStackTrace();
      } catch (IllegalAccessException e) {
	e.printStackTrace();
      }
    }
  }

  protected static Map<String, String> elementMap = new HashMap<String, String>();

  // elementName --> elementClassName

  protected static Map<String, String> adderMap = new HashMap<String, String>();

  // elementName --> adder method name (without "add"/"set" prefix)

  protected static final boolean debug = false;

}
